package com.nhncorp.yammer.util;

import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

/**
 * <h3><b>년월일시분초 형식의 날짜 객체</b></h3> 클라이언트 입력값과 sqlmap 바인딩 시의 날짜 처리를 제외하고 서비스 코드
 * 내의 모든 날짜 처리 부분은 이 객체를 사용한다.
 * 
 * @author KR11783. 2010. 06. 17. (목) 13:53:18 KST
 */
public class Datetime implements DatetimeComparable, DatetimeCalculatable, Comparable<Datetime>, Cloneable {
	/**
	 * 날짜 데이터를 저장하기 위한 java.util.Calendar 멤버 변수
	 */
	final protected Calendar calendar = Calendar.getInstance();

	/**
	 * 날짜 값 변경 가능 여부
	 */
	private boolean mutable = true;

	private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

	private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";

	private static final String DASH_DATE_PATTERN = "yyyy.MM.dd";

	private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";

	private static final String HOUR_MINUTE_TIME_PATTERN = "HH:mm";

	public static final int SUNDAY = Calendar.SUNDAY;
	public static final int MONDAY = Calendar.MONDAY;
	public static final int TUESDAY = Calendar.TUESDAY;
	public static final int THURSDAY = Calendar.THURSDAY;
	public static final int FRIDAY = Calendar.FRIDAY;
	public static final int SATURDAY = Calendar.SATURDAY;

	public Datetime() {
		mutable = true;
		flattenMilliSecond(calendar);
	}

	private void setDateValue(java.util.Date javaUtilDate) {
		calendar.setTime(javaUtilDate);
		flattenMilliSecond(calendar);
	}

	public Calendar getCalendar() {
		return calendar;
	}

	/**
	 * java.util.Date 객체로 날짜 생성
	 * 
	 * @param javaUtilDate
	 */
	public Datetime(java.util.Date javaUtilDate) {
		setDateValue(javaUtilDate);
	}

	/**
	 * java.sql.Date 객체로 날짜 생성
	 * 
	 * @param javaSqlDate
	 */
	public Datetime(java.sql.Date javaSqlDate) {
		setDateValue(javaSqlDate);
	}

	public Datetime(String datetimeString, SimpleDateFormat formatter) {
		try {
			setDateValue(formatter.parse(datetimeString));
		} catch (ParseException e) {
			try {
				setDateValue(new SimpleDateFormat(DEFAULT_DATE_PATTERN).parse(datetimeString));
			} catch (ParseException ee) {
				throw new RuntimeException("[" + datetimeString + "] is wrong datetime format.");
			}
		}
	}

	public Datetime(String datetimeString) {
		this(datetimeString, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));
	}

	public Datetime(String datetimeString, boolean mutable) {
		this(datetimeString);
		this.mutable = mutable;
	}

	public Datetime(String datetimeString, String datetimePattern) {
		this(datetimeString, new SimpleDateFormat(datetimePattern));
	}

	public Datetime(Timestamp timestamp) {
		setDateValue(timestamp);
	}

	public Datetime(Datetime datetime) {
		setDateValue(datetime.calendar.getTime());
	}

	public String toString(SimpleDateFormat formatter) {
		return formatter.format(calendar.getTime());
	}

	@Override
	public String toString() {
		return toString(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));
	}

	public String toString(String datetimePattern) {
		return toString(new SimpleDateFormat(datetimePattern));
	}

	public String getValue() {
		return toString();
	}

	public java.util.Date toJavaUtilDate() {
		return calendar.getTime();
	}

	/**
	 * yyyy-MM-dd 형식의 스트링으로 출력
	 * 
	 * @return
	 */
	public String getStringDate() {
		return toString(new SimpleDateFormat(DEFAULT_DATE_PATTERN));
	}

	/**
	 * yyyy.MM.dd 형식의 스트링으로 출력
	 * 
	 * @return
	 */
	public String getDashStringDate() {
		return toString(new SimpleDateFormat(DASH_DATE_PATTERN));
	}

	public String getStringTime() {
		return toString(new SimpleDateFormat(DEFAULT_TIME_PATTERN));
	}

	public String getStringHourMinute() {
		return toString(new SimpleDateFormat(HOUR_MINUTE_TIME_PATTERN));
	}

	public int getYear() {
		return calendar.get(Calendar.YEAR);
	}

	public int getMonth() {
		return calendar.get(Calendar.MONTH);
	}

	public int getDay() {
		return calendar.get(Calendar.DAY_OF_MONTH);
	}

	public int getHour() {
		return calendar.get(Calendar.HOUR_OF_DAY);
	}

	public int getHour12() {
		if (getHour() == 12 || getHour() == 0) {
			return 12;
		}
		return calendar.get(Calendar.HOUR);
	}

	public int getMinute() {
		return calendar.get(Calendar.MINUTE);
	}

	public int getSecond() {
		return calendar.get(Calendar.SECOND);
	}

	public int getDayOfWeekInMonth() {
		return calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
	}

	public String getStringDateKor() {
		return getYear() + "년 " + (getMonth() + 1) + "월 " + getDay() + "일";
	}

	public String getStringDateKorExceptYear() {
		return (getMonth() + 1) + "월 " + getDay() + "일";
	}

	public String getAmPmKor() {
		return getHour() >= 12 ? "오후" : "오전";
	}

	public String getHourMinute12() {
		return String.format("%02d:%02d", getHour12(), getMinute());
	}

	/**
	 * 이 날짜 객체가 연중 몇 번째 날인지 반환한다.
	 * 
	 * @return 연중 몇 번째 날인지 반환
	 */
	public int getDayOfYear() {
		return calendar.get(Calendar.DAY_OF_YEAR);
	}

	/**
	 * 이 날짜 객체가 연중 몇 번째 주인지 반환한다.
	 * 
	 * @return 연중 몇 번째 주인지 반환
	 */
	public int getWeekOfYear() {
		return calendar.get(Calendar.WEEK_OF_YEAR);
	}

	/**
	 * 일주일 중 몇 번째 요일인지 반환한다. 일요일을 시작으로 토요일까지 1부터 7까지의 수가 할당되어 반환된다.<br>
	 * 즉,<br>
	 * 일요일 - 1<br>
	 * 월요일 - 2<br>
	 * 화요일 - 3<br>
	 * 수요일 - 4<br>
	 * 목요일 - 5<br>
	 * 금요일 - 6<br>
	 * 토요일 - 7<br>
	 * 
	 * @return 일요일을 시작으로 하는 일주일 중 몇 번째 날인지 반환
	 */
	public int getDayOfWeek() {
		return calendar.get(Calendar.DAY_OF_WEEK);
	}

	/**
	 * 월 중 몇 번째 주인지 반환
	 * 
	 * @return
	 */
	public int getWeekOfMonth() {
		return calendar.get(Calendar.WEEK_OF_MONTH);
	}

	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	//
	// DatetimeCalculatable
	//
	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	private void checkMutable() {
		if (!mutable) {
			throw new RuntimeException("Not allowed action. I am immutable!");
		}
	}

	/**
	 * 시간 이하를 모두 0으로 세팅
	 * 
	 * @param calendar
	 */
	private void flatten(Calendar calendar) {
		calendar.set(Calendar.HOUR_OF_DAY, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		flush();
	}

	public Datetime flatten() {
		checkMutable();
		flatten(calendar);
		return this;
	}

	public Datetime fill() {
		checkMutable();
		calendar.set(Calendar.HOUR_OF_DAY, 23);
		calendar.set(Calendar.MINUTE, 59);
		calendar.set(Calendar.SECOND, 59);
		flush();
		return this;
	}

	/**
	 * Set millisecond to 0. (Ignore)
	 * 
	 * @param calendar
	 */
	private void flattenMilliSecond(Calendar calendar) {
		calendar.set(Calendar.MILLISECOND, 0);
		flush();
	}

	// compareTo

	@Override
	public int compareTo(Datetime target, boolean compareOnlyDate) {
		if (target == null) {
			return -1;
		}

		if (compareOnlyDate) {
			Calendar sourceCal = (Calendar)calendar.clone();
			Calendar targetCal = (Calendar)target.calendar.clone();
			flatten(sourceCal);
			flatten(targetCal);

			if (sourceCal.before(targetCal)) {
				return -1;
			} else if (sourceCal.equals(targetCal)) {
				return 0;
			} else {
				return 1;
			}
		} else {
			return calendar.compareTo(target.calendar);
		}
	}

	// equals

	@Override
	public boolean equals(Datetime target, boolean compareOnlyDate) {
		return compareTo(target, compareOnlyDate) == 0;
	}

	@Override
	public boolean equals(Object object) {
		if (object == null) {
			return false;
		}

		// 같은 Datetime과의 비교
		if (this.getClass().equals(object.getClass()) && equals((Datetime)object, false)) {
			return true;
		}

		// LunarDatetime과의 비교
		//		if (LunarDatetime.class.equals(object.getClass()) && equals(((LunarDatetime)object).toSolar(), false)) {
		//			return true;
		//		}

		return false;
	}

	// before

	@Override
	public boolean before(Datetime target, boolean compareOnlyDate) {
		return compareTo(target, compareOnlyDate) < 0;
	}

	@Override
	public boolean before(Datetime target) {
		return compareTo(target, false) < 0;
	}

	@Override
	public boolean before(boolean compareOnlyDate) {
		return compareTo(new Datetime(), compareOnlyDate) < 0;
	}

	@Override
	public boolean before() {
		return compareTo(new Datetime(), false) < 0;
	}

	// beforeOrEquals

	@Override
	public boolean beforeOrEquals(Datetime target, boolean compareOnlyDate) {
		return compareTo(target, compareOnlyDate) <= 0;
	}

	@Override
	public boolean beforeOrEquals(boolean compareOnlyDate) {
		return compareTo(new Datetime(), compareOnlyDate) <= 0;
	}

	@Override
	public boolean beforeOrEquals(Datetime target) {
		return compareTo(target, false) <= 0;
	}

	@Override
	public boolean beforeOrEquals() {
		return compareTo(new Datetime(), false) <= 0;
	}

	// after

	@Override
	public boolean after(Datetime target, boolean compareOnlyDate) {
		return compareTo(target, compareOnlyDate) > 0;
	}

	@Override
	public boolean after(Datetime target) {
		return compareTo(target, false) > 0;
	}

	@Override
	public boolean after(boolean compareOnlyDate) {
		return compareTo(new Datetime(), compareOnlyDate) > 0;
	}

	@Override
	public boolean after() {
		return compareTo(new Datetime(), false) > 0;
	}

	@Override
	public boolean afterOrEquals(Datetime target, boolean compareOnlyDate) {
		return compareTo(target, compareOnlyDate) >= 0;
	}

	@Override
	public boolean afterOrEquals(Datetime target) {
		return compareTo(target, false) >= 0;
	}

	@Override
	public boolean afterOrEquals(boolean compareOnlyDate) {
		return compareTo(new Datetime(), compareOnlyDate) >= 0;
	}

	@Override
	public boolean afterOrEquals() {
		return compareTo(new Datetime(), false) >= 0;
	}

	/**
	 * 이번 주(월요일에서 일요일 사이)에 속하는지 여부를 반환한다.
	 * 
	 * @return 이번주에 속하면 true, 그렇지 않으면 false
	 */
	@Override
	public boolean isThisWeek() {
		Datetime now = new Datetime();
		Datetime monday = new Datetime(now.increaseDay(-(now.getDayOfWeek() + 5) % 7));
		return afterOrEquals(monday) && before(monday.increaseDay(7));
	}

	/**
	 * 주어진 시작일에서 종료일 사이(boundary 포함)에 해당하는지 반환
	 * 
	 * @param start
	 * @param end
	 * @return
	 */
	@Override
	public boolean between(Datetime start, Datetime end, boolean compareOnlyDate) {
		return afterOrEquals(min(start, end), compareOnlyDate) && beforeOrEquals(max(start, end), compareOnlyDate);
	}

	/**
	 * 주어진 시작일과 종료일 바깥에 있는지 반환. (boundary 바깥에 있어야 함)
	 * 
	 * @param start
	 * @param end
	 * @param compareOnlyDate
	 * @return
	 * @see com.naver.cal.commons.data.date.DatetimeComparable#outOf(com.naver.cal.commons.data.date.Datetime,
	 *      com.naver.cal.commons.data.date.Datetime, boolean)
	 */
	@Override
	public boolean outOf(Datetime start, Datetime end, boolean compareOnlyDate) {
		return before(min(start, end), compareOnlyDate) || after(max(start, end), compareOnlyDate);
	}

	@Override
	public boolean between(Datetime start, Datetime end) {
		return between(start, end, false);
	}

	@Override
	public boolean outOf(Datetime start, Datetime end) {
		return outOf(start, end, false);
	}

	private Datetime min(Datetime objA, Datetime objB) {
		return objA != null && objB != null && objA.before(objB) ? objA : objB;
	}

	private Datetime max(Datetime objA, Datetime objB) {
		return objA != null && objB != null && objA.after(objB) ? objA : objB;
	}

	/**
	 * 이 객체에 초,분,시,일을 더한다. 객체 자체의 날짜가 변경됨.
	 * 
	 * @param second 초
	 * @param minute 분
	 * @param hour 시간
	 * @param day 일
	 * @param month 월
	 * @param year 년
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increase(int second, int minute, int hour, int day, int month, int year) {
		checkMutable();
		calendar.add(Calendar.SECOND, second);
		calendar.add(Calendar.MINUTE, minute);
		calendar.add(Calendar.HOUR_OF_DAY, hour);
		calendar.add(Calendar.DATE, day);
		calendar.add(Calendar.MONTH, month);
		calendar.add(Calendar.YEAR, year);
		flush();
		return this;
	}

	public Datetime increaseMilliSecond(long millis) {
		calendar.setTimeInMillis(calendar.getTimeInMillis() + millis);
		flush();
		return this;
	}

	/**
	 * 이 객체에 초를 더한다.
	 * 
	 * @param second 초
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increaseSecond(int second) {
		return increase(second, 0, 0, 0, 0, 0);
	}

	/**
	 * 이 객체에 분을 더한다.
	 * 
	 * @param minute 분
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increaseMinute(int minute) {
		return increase(0, minute, 0, 0, 0, 0);
	}

	/**
	 * 이 객체에 시간을 더한다.
	 * 
	 * @param hour 시간
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increaseHour(int hour) {
		return increase(0, 0, hour, 0, 0, 0);
	}

	/**
	 * 이 객체에 일을 더한다.
	 * 
	 * @param day 일
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increaseDay(int day) {
		return increase(0, 0, 0, day, 0, 0);
	}

	/**
	 * 이 객체에 월을 더한다.
	 * 
	 * @param month 월
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increaseMonth(int month) {
		return increase(0, 0, 0, 0, month, 0);
	}

	/**
	 * 이 객체에 년을 더한다.
	 * 
	 * @param year 년
	 * @return 변경된 객체 자신
	 */
	@Override
	public Datetime increaseYear(int year) {
		return increase(0, 0, 0, 0, 0, year);
	}

	/**
	 * 이 날짜에 주어진 값을 더한 새로운 날짜 객체를 반한
	 * 
	 * @param second 초
	 * @param minute 분
	 * @param hour 시간
	 * @param day 일
	 * @return
	 */
	@Override
	public Datetime add(int second, int minute, int hour, int day, int month, int year) {
		checkMutable();
		return clone().increase(second, minute, hour, day, month, year);
	}

	/**
	 * 이 날짜에 주어진 초를 더한 새로운 날짜 객체를 반환
	 * 
	 * @param second 초
	 * @return
	 */
	@Override
	public Datetime addSecond(int second) {
		return add(second, 0, 0, 0, 0, 0);
	}

	/**
	 * 이 날짜에 주어진 분을 더한 새로운 날짜 객체를 반환
	 * 
	 * @param minute 분
	 * @return
	 */
	@Override
	public Datetime addMinute(int minute) {
		return add(0, minute, 0, 0, 0, 0);
	}

	/**
	 * 이 날짜에 주어진 시간을 더한 새로운 날짜 객체를 반환
	 * 
	 * @param hour 시간
	 * @return
	 */
	@Override
	public Datetime addHour(int hour) {
		return add(0, 0, hour, 0, 0, 0);
	}

	/**
	 * 이 날짜에 주어진 일을 더한 새로운 날짜 객체를 반환
	 * 
	 * @param day 일
	 * @return
	 */
	@Override
	public Datetime addDay(int day) {
		return add(0, 0, 0, day, 0, 0);
	}

	/**
	 * 이 날짜에 주어진 월을 더한 새로운 날짜 객체를 반환
	 * 
	 * @param month 월
	 * @return
	 */
	@Override
	public Datetime addMonth(int month) {
		return add(0, 0, 0, 0, month, 0);
	}

	/**
	 * 이 날짜에 주어진 연을 더한 새로운 날짜 객체를 반환
	 * 
	 * @param year 년
	 * @return
	 */
	@Override
	public Datetime addYear(int year) {
		return add(0, 0, 0, 0, 0, year);
	}

	@Override
	public int hashCode() {
		return 56544321;
	}

	/**
	 * 이 객체와 같은 날짜 값을 가지는 새로운 복사본을 반환한다. immutable 객체를 clone할 경우 mutable한 새 객체를 반환한다.
	 */
	@Override
	public Datetime clone() {
		Datetime clone = new Datetime(calendar.getTime());
		clone.mutable = true;
		return clone;
	}

	@Override
	public Datetime max(Datetime target) {
		return max(this, target);
	}

	@Override
	public Datetime min(Datetime target) {
		return min(this, target);
	}

	@Override
	public int compareTo(Datetime object) {
		return compareTo(object, false);
	}

	@Override
	public int getDayDiff(Datetime target) {
		if (target == null) {
			return -1;
		}
		return Math.abs((int)((target.calendar.getTimeInMillis() - calendar.getTimeInMillis()) / 86400000L));
	}

	@Override
	public long getTimeDiff(Datetime target) {
		if (target == null) {
			return -1;
		}
		long start = calendar.getTimeInMillis() + calendar.getTimeZone().getOffset(calendar.getTimeInMillis());
		long end = target.getCalendar().getTimeInMillis() + target.getCalendar().getTimeZone().getOffset(target.getCalendar().getTimeInMillis());
		return end - start;
	}

	/**
	 * calendar 객체의 변경값을 flush한다.
	 */
	private void flush() {
		calendar.getTimeInMillis();
	}

	public Datetime set(Datetime datetime) {
		checkMutable();
		setDateValue(datetime.calendar.getTime());
		return this;
	}

	/**
	 * 입력 날짜 객체의 년월일 값을 세팅
	 * 
	 * @param datetime
	 * @return
	 */
	public Datetime setDate(Datetime datetime) {
		checkMutable();
		long timeValue = getHour() * 3600 + getMinute() * 60 + getSecond();
		Datetime time = datetime.clone().flatten();
		calendar.setTimeInMillis(time.getTimeInMillis() + timeValue * 1000);
		flush();
		return this;
	}

	/**
	 * 입력 날짜 객체의 시분초 값을 세팅
	 * 
	 * @param datetime
	 * @return
	 */
	public Datetime setTime(Datetime datetime) {
		checkMutable();
		flatten();
		long timeValue = datetime.getHour() * 3600 + datetime.getMinute() * 60 + datetime.getSecond();
		calendar.setTimeInMillis(getTimeInMillis() + timeValue * 1000);
		flush();
		return this;
	}

	public Datetime setSecond(int second) {
		checkMutable();
		calendar.set(Calendar.SECOND, second);
		flush();
		return this;
	}

	public Datetime setMinute(int minute) {
		checkMutable();
		calendar.set(Calendar.MINUTE, minute);
		flush();
		return this;
	}

	public Datetime setHour(int hour) {
		checkMutable();
		calendar.set(Calendar.HOUR_OF_DAY, hour);
		flush();
		return this;
	}

	public Datetime setDay(int day) {
		checkMutable();
		calendar.set(Calendar.DAY_OF_MONTH, day);
		flush();
		return this;
	}

	public Datetime setMonth(int month) {
		checkMutable();
		calendar.set(Calendar.MONTH, month);
		flush();
		return this;
	}

	public Datetime setYear(int year) {
		checkMutable();
		calendar.set(Calendar.YEAR, year);
		flush();
		return this;
	}

	public Datetime setWeekOfMonth(int weekOfMonth) {
		checkMutable();
		calendar.set(Calendar.WEEK_OF_MONTH, weekOfMonth);
		flush();
		return this;
	}

	public Datetime setDayOfWeek(int dayOfWeek) {
		checkMutable();
		calendar.set(Calendar.DAY_OF_WEEK, dayOfWeek);
		flush();
		return this;
	}

	public Datetime setDayOfWeekInMonth(int dayOfWeekInMonth) {
		checkMutable();
		calendar.set(Calendar.DAY_OF_WEEK_IN_MONTH, dayOfWeekInMonth);
		flush();
		return this;
	}

	public int getActualMaximumDay() {
		return calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
	}

	public Datetime setActualMaximumDay() {
		checkMutable();
		return setDay(calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
	}

	public int getActualMaximumWeek() {
		return calendar.getActualMaximum(Calendar.WEEK_OF_MONTH);
	}

	public Datetime setActualMaximumWeek() {
		checkMutable();
		return setWeekOfMonth(calendar.getActualMaximum(Calendar.WEEK_OF_MONTH));
	}

	public long getTimeInMillis() {
		return calendar.getTimeInMillis();
	}

	/**
	 * 현재 시간을 반환한다.
	 * @return
	 */
	public static Datetime getToday() {
		return new Datetime();
	}
}